diff options
| author | joonhoekim <26rote@gmail.com> | 2025-10-23 18:44:19 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-10-23 18:44:19 +0900 |
| commit | 04bd1965c3699a4b29ed9c9627574bfeedd3d6c6 (patch) | |
| tree | 691b9a6e844a788937a240d47e77e8cfa848a88a /app/[lng] | |
| parent | 535e234dbd674bf2e5ecf344e03ed8ae5b2cbd6c (diff) | |
(김준회) SWP 문서 업로드 (Submisssion) 초기 개발건
Diffstat (limited to 'app/[lng]')
4 files changed, 576 insertions, 0 deletions
diff --git a/app/[lng]/evcp/(evcp)/(eng)/swp-document-upload/page.tsx b/app/[lng]/evcp/(evcp)/(eng)/swp-document-upload/page.tsx new file mode 100644 index 00000000..25a0bfe6 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/(eng)/swp-document-upload/page.tsx @@ -0,0 +1,58 @@ +import { Suspense } from "react"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import SwpDocumentPage from "./swp-document-page"; + +export const metadata = { + title: "SWP 문서 관리", + description: "SWP 시스템 문서 조회 및 동기화", +}; + +// ============================================================================ +// 로딩 스켈레톤 +// ============================================================================ + +function SwpDocumentSkeleton() { + return ( + <Card> + <CardHeader> + <div className="flex items-center justify-between"> + <Skeleton className="h-8 w-32" /> + <Skeleton className="h-10 w-40" /> + </div> + </CardHeader> + <CardContent className="space-y-4"> + <Skeleton className="h-32 w-full" /> + <Skeleton className="h-96 w-full" /> + </CardContent> + </Card> + ); +} + +export default async function SwpDocumentUploadPage({ + searchParams, +}: { + searchParams: Promise<{ [key: string]: string | string[] | undefined }>; +}) { + const params = await searchParams; + + return ( + <div className="container mx-auto py-6 space-y-6"> + {/* 헤더 */} + <Card> + <CardHeader> + <CardTitle className="text-2xl">SWP 문서 관리</CardTitle> + <CardDescription> + 외부 시스템(SWP)에서 문서 및 첨부파일을 조회하고 동기화합니다. + 문서 → 리비전 → 파일 계층 구조로 확인할 수 있습니다. + </CardDescription> + </CardHeader> + </Card> + + {/* 메인 컨텐츠 */} + <Suspense fallback={<SwpDocumentSkeleton />}> + <SwpDocumentPage searchParams={params} /> + </Suspense> + </div> + ); +} diff --git a/app/[lng]/evcp/(evcp)/(eng)/swp-document-upload/swp-document-page.tsx b/app/[lng]/evcp/(evcp)/(eng)/swp-document-upload/swp-document-page.tsx new file mode 100644 index 00000000..eedb68e2 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/(eng)/swp-document-upload/swp-document-page.tsx @@ -0,0 +1,231 @@ +"use client"; + +import { useState, useEffect, useTransition } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Skeleton } from "@/components/ui/skeleton"; +import { InfoIcon } from "lucide-react"; +import { SwpTable } from "@/lib/swp/table/swp-table"; +import { SwpTableToolbar } from "@/lib/swp/table/swp-table-toolbar"; +import { + fetchSwpDocuments, + fetchProjectList, + fetchSwpStats, + type SwpTableFilters, + type SwpDocumentWithStats, +} from "@/lib/swp/actions"; + +interface SwpDocumentPageProps { + searchParams: { [key: string]: string | string[] | undefined }; +} + +export default function SwpDocumentPage({ searchParams }: SwpDocumentPageProps) { + const router = useRouter(); + const params = useSearchParams(); + const [isPending, startTransition] = useTransition(); + + // URL에서 필터 파라미터 추출 + const initialFilters: SwpTableFilters = { + projNo: (searchParams.projNo as string) || "", + docNo: (searchParams.docNo as string) || "", + docTitle: (searchParams.docTitle as string) || "", + pkgNo: (searchParams.pkgNo as string) || "", + vndrCd: (searchParams.vndrCd as string) || "", + stage: (searchParams.stage as string) || "", + }; + + const initialPage = parseInt((searchParams.page as string) || "1", 10); + const initialPageSize = parseInt((searchParams.pageSize as string) || "100", 10); + + // 상태 관리 + const [documents, setDocuments] = useState<SwpDocumentWithStats[]>([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(initialPage); + const [pageSize] = useState(initialPageSize); + const [totalPages, setTotalPages] = useState(0); + const [filters, setFilters] = useState<SwpTableFilters>(initialFilters); + const [projects, setProjects] = useState<Array<{ PROJ_NO: string; PROJ_NM: string }>>([]); + const [stats, setStats] = useState({ + total_documents: 0, + total_revisions: 0, + total_files: 0, + last_sync: null as Date | null, + }); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState<string | null>(null); + + // 초기 데이터 로드 + useEffect(() => { + loadInitialData(); + }, []); + + // 필터 변경 시 데이터 재로드 + useEffect(() => { + if (!isLoading) { + loadDocuments(); + } + }, [filters, page]); + + const loadInitialData = async () => { + try { + setIsLoading(true); + setError(null); + + // 병렬로 데이터 로드 + const [projectsData, statsData, documentsData] = await Promise.all([ + fetchProjectList(), + fetchSwpStats(), + fetchSwpDocuments({ + page, + pageSize, + filters: Object.keys(initialFilters).length > 0 ? initialFilters : undefined, + }), + ]); + + setProjects(projectsData); + setStats(statsData); + setDocuments(documentsData.data); + setTotal(documentsData.total); + setTotalPages(documentsData.totalPages); + } catch (err) { + console.error("초기 데이터 로드 실패:", err); + setError(err instanceof Error ? err.message : "데이터 로드 실패"); + } finally { + setIsLoading(false); + } + }; + + const loadDocuments = async () => { + startTransition(async () => { + try { + const data = await fetchSwpDocuments({ + page, + pageSize, + filters: Object.keys(filters).some((key) => filters[key as keyof SwpTableFilters]) + ? filters + : undefined, + }); + + setDocuments(data.data); + setTotal(data.total); + setTotalPages(data.totalPages); + + // URL 업데이트 + const params = new URLSearchParams(); + if (filters.projNo) params.set("projNo", filters.projNo); + if (filters.docNo) params.set("docNo", filters.docNo); + if (filters.docTitle) params.set("docTitle", filters.docTitle); + if (filters.pkgNo) params.set("pkgNo", filters.pkgNo); + if (filters.vndrCd) params.set("vndrCd", filters.vndrCd); + if (filters.stage) params.set("stage", filters.stage); + if (page !== 1) params.set("page", page.toString()); + + router.push(`?${params.toString()}`, { scroll: false }); + } catch (err) { + console.error("문서 로드 실패:", err); + setError(err instanceof Error ? err.message : "문서 로드 실패"); + } + }); + }; + + const handleFiltersChange = (newFilters: SwpTableFilters) => { + setFilters(newFilters); + setPage(1); // 필터 변경 시 첫 페이지로 + }; + + const handlePageChange = (newPage: number) => { + setPage(newPage); + }; + + if (isLoading) { + return ( + <Card> + <CardHeader> + <Skeleton className="h-8 w-48" /> + <Skeleton className="h-4 w-96" /> + </CardHeader> + <CardContent className="space-y-4"> + <Skeleton className="h-32 w-full" /> + <Skeleton className="h-96 w-full" /> + </CardContent> + </Card> + ); + } + + if (error) { + return ( + <Alert variant="destructive"> + <AlertDescription>{error}</AlertDescription> + </Alert> + ); + } + + return ( + <div className="space-y-6"> + {/* 통계 카드 */} + <div className="grid grid-cols-1 md:grid-cols-4 gap-4"> + <Card> + <CardHeader className="pb-3"> + <CardDescription>총 문서</CardDescription> + <CardTitle className="text-3xl">{stats.total_documents.toLocaleString()}</CardTitle> + </CardHeader> + </Card> + <Card> + <CardHeader className="pb-3"> + <CardDescription>총 리비전</CardDescription> + <CardTitle className="text-3xl">{stats.total_revisions.toLocaleString()}</CardTitle> + </CardHeader> + </Card> + <Card> + <CardHeader className="pb-3"> + <CardDescription>총 파일</CardDescription> + <CardTitle className="text-3xl">{stats.total_files.toLocaleString()}</CardTitle> + </CardHeader> + </Card> + <Card> + <CardHeader className="pb-3"> + <CardDescription>마지막 동기화</CardDescription> + <CardTitle className="text-lg"> + {stats.last_sync + ? new Date(stats.last_sync).toLocaleDateString("ko-KR") + : "없음"} + </CardTitle> + </CardHeader> + </Card> + </div> + + {/* 안내 메시지 */} + {documents.length === 0 && !filters.projNo && ( + <Alert> + <InfoIcon className="h-4 w-4" /> + <AlertDescription> + 시작하려면 프로젝트를 선택하고 <strong>SWP 동기화</strong> 버튼을 클릭하세요. + </AlertDescription> + </Alert> + )} + + {/* 메인 테이블 */} + <Card> + <CardHeader> + <SwpTableToolbar + filters={filters} + onFiltersChange={handleFiltersChange} + projects={projects} + /> + </CardHeader> + <CardContent> + <SwpTable + initialData={documents} + total={total} + page={page} + pageSize={pageSize} + totalPages={totalPages} + onPageChange={handlePageChange} + /> + </CardContent> + </Card> + </div> + ); +} + diff --git a/app/[lng]/partners/(partners)/swp-document-upload/page.tsx b/app/[lng]/partners/(partners)/swp-document-upload/page.tsx new file mode 100644 index 00000000..25eb52aa --- /dev/null +++ b/app/[lng]/partners/(partners)/swp-document-upload/page.tsx @@ -0,0 +1,57 @@ +import { Suspense } from "react"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import VendorDocumentPage from "./vendor-document-page"; + +export const metadata = { + title: "문서 조회 및 업로드", + description: "협력업체 문서 조회 및 파일 업로드", +}; + +// ============================================================================ +// 로딩 스켈레톤 +// ============================================================================ + +function VendorDocumentSkeleton() { + return ( + <Card> + <CardHeader> + <div className="flex items-center justify-between"> + <Skeleton className="h-8 w-32" /> + <Skeleton className="h-10 w-40" /> + </div> + </CardHeader> + <CardContent className="space-y-4"> + <Skeleton className="h-32 w-full" /> + <Skeleton className="h-96 w-full" /> + </CardContent> + </Card> + ); +} + +export default async function DocumentUploadPage({ + searchParams, +}: { + searchParams: Promise<{ [key: string]: string | string[] | undefined }>; +}) { + const params = await searchParams; + + return ( + <div className="container mx-auto py-6 space-y-6"> + {/* 헤더 */} + <Card> + <CardHeader> + <CardTitle className="text-2xl">문서 조회 및 업로드</CardTitle> + <CardDescription> + 프로젝트별 할당된 문서를 조회하고 파일을 업로드할 수 있습니다. + </CardDescription> + </CardHeader> + </Card> + + {/* 메인 컨텐츠 */} + <Suspense fallback={<VendorDocumentSkeleton />}> + <VendorDocumentPage searchParams={params} /> + </Suspense> + </div> + ); +}
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/swp-document-upload/vendor-document-page.tsx b/app/[lng]/partners/(partners)/swp-document-upload/vendor-document-page.tsx new file mode 100644 index 00000000..f2469c29 --- /dev/null +++ b/app/[lng]/partners/(partners)/swp-document-upload/vendor-document-page.tsx @@ -0,0 +1,230 @@ +"use client"; + +import { useState, useEffect, useTransition } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Skeleton } from "@/components/ui/skeleton"; +import { InfoIcon } from "lucide-react"; +import { SwpTable } from "@/lib/swp/table/swp-table"; +import { SwpTableToolbar } from "@/lib/swp/table/swp-table-toolbar"; +import { + fetchVendorDocuments, + fetchVendorProjects, + fetchVendorSwpStats, + type SwpTableFilters, + type SwpDocumentWithStats, +} from "@/lib/swp/vendor-actions"; + +interface VendorDocumentPageProps { + searchParams: { [key: string]: string | string[] | undefined }; +} + +export default function VendorDocumentPage({ searchParams }: VendorDocumentPageProps) { + const router = useRouter(); + const params = useSearchParams(); + const [isPending, startTransition] = useTransition(); + + // URL에서 필터 파라미터 추출 (vndrCd는 제외 - 서버에서 자동 설정) + const initialFilters: SwpTableFilters = { + projNo: (searchParams.projNo as string) || "", + docNo: (searchParams.docNo as string) || "", + docTitle: (searchParams.docTitle as string) || "", + pkgNo: (searchParams.pkgNo as string) || "", + stage: (searchParams.stage as string) || "", + }; + + const initialPage = parseInt((searchParams.page as string) || "1", 10); + const initialPageSize = parseInt((searchParams.pageSize as string) || "100", 10); + + // 상태 관리 + const [documents, setDocuments] = useState<SwpDocumentWithStats[]>([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(initialPage); + const [pageSize] = useState(initialPageSize); + const [totalPages, setTotalPages] = useState(0); + const [filters, setFilters] = useState<SwpTableFilters>(initialFilters); + const [projects, setProjects] = useState<Array<{ PROJ_NO: string; PROJ_NM: string }>>([]); + const [stats, setStats] = useState({ + total_documents: 0, + total_revisions: 0, + total_files: 0, + uploaded_files: 0, + last_sync: null as Date | null, + }); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState<string | null>(null); + + // 초기 데이터 로드 + useEffect(() => { + loadInitialData(); + }, []); + + // 필터 변경 시 데이터 재로드 + useEffect(() => { + if (!isLoading) { + loadDocuments(); + } + }, [filters, page]); + + const loadInitialData = async () => { + try { + setIsLoading(true); + setError(null); + + // 병렬로 데이터 로드 + const [projectsData, statsData, documentsData] = await Promise.all([ + fetchVendorProjects(), + fetchVendorSwpStats(), + fetchVendorDocuments({ + page, + pageSize, + filters: Object.keys(initialFilters).length > 0 ? initialFilters : undefined, + }), + ]); + + setProjects(projectsData); + setStats(statsData); + setDocuments(documentsData.data); + setTotal(documentsData.total); + setTotalPages(documentsData.totalPages); + } catch (err) { + console.error("초기 데이터 로드 실패:", err); + setError(err instanceof Error ? err.message : "데이터 로드 실패"); + } finally { + setIsLoading(false); + } + }; + + const loadDocuments = async () => { + startTransition(async () => { + try { + const data = await fetchVendorDocuments({ + page, + pageSize, + filters: Object.keys(filters).some((key) => filters[key as keyof SwpTableFilters]) + ? filters + : undefined, + }); + + setDocuments(data.data); + setTotal(data.total); + setTotalPages(data.totalPages); + + // URL 업데이트 + const params = new URLSearchParams(); + if (filters.projNo) params.set("projNo", filters.projNo); + if (filters.docNo) params.set("docNo", filters.docNo); + if (filters.docTitle) params.set("docTitle", filters.docTitle); + if (filters.pkgNo) params.set("pkgNo", filters.pkgNo); + if (filters.stage) params.set("stage", filters.stage); + if (page !== 1) params.set("page", page.toString()); + + router.push(`?${params.toString()}`, { scroll: false }); + } catch (err) { + console.error("문서 로드 실패:", err); + setError(err instanceof Error ? err.message : "문서 로드 실패"); + } + }); + }; + + const handleFiltersChange = (newFilters: SwpTableFilters) => { + setFilters(newFilters); + setPage(1); // 필터 변경 시 첫 페이지로 + }; + + const handlePageChange = (newPage: number) => { + setPage(newPage); + }; + + if (isLoading) { + return ( + <Card> + <CardHeader> + <Skeleton className="h-8 w-48" /> + <Skeleton className="h-4 w-96" /> + </CardHeader> + <CardContent className="space-y-4"> + <Skeleton className="h-32 w-full" /> + <Skeleton className="h-96 w-full" /> + </CardContent> + </Card> + ); + } + + if (error) { + return ( + <Alert variant="destructive"> + <AlertDescription>{error}</AlertDescription> + </Alert> + ); + } + + return ( + <div className="space-y-6"> + {/* 통계 카드 */} + <div className="grid grid-cols-1 md:grid-cols-4 gap-4"> + <Card> + <CardHeader className="pb-3"> + <CardDescription>할당된 문서</CardDescription> + <CardTitle className="text-3xl">{stats.total_documents.toLocaleString()}</CardTitle> + </CardHeader> + </Card> + <Card> + <CardHeader className="pb-3"> + <CardDescription>총 리비전</CardDescription> + <CardTitle className="text-3xl">{stats.total_revisions.toLocaleString()}</CardTitle> + </CardHeader> + </Card> + <Card> + <CardHeader className="pb-3"> + <CardDescription>총 파일</CardDescription> + <CardTitle className="text-3xl">{stats.total_files.toLocaleString()}</CardTitle> + </CardHeader> + </Card> + <Card> + <CardHeader className="pb-3"> + <CardDescription>업로드한 파일</CardDescription> + <CardTitle className="text-3xl text-green-600"> + {stats.uploaded_files.toLocaleString()} + </CardTitle> + </CardHeader> + </Card> + </div> + + {/* 안내 메시지 */} + {documents.length === 0 && !filters.projNo && ( + <Alert> + <InfoIcon className="h-4 w-4" /> + <AlertDescription> + 프로젝트를 선택하여 할당된 문서를 확인하세요. + </AlertDescription> + </Alert> + )} + + {/* 메인 테이블 */} + <Card> + <CardHeader> + <SwpTableToolbar + filters={filters} + onFiltersChange={handleFiltersChange} + projects={projects} + mode="vendor" + /> + </CardHeader> + <CardContent> + <SwpTable + initialData={documents} + total={total} + page={page} + pageSize={pageSize} + totalPages={totalPages} + onPageChange={handlePageChange} + mode="vendor" + /> + </CardContent> + </Card> + </div> + ); +} + |
